--cgilua.contentheader ("text", "plain")

-------------------------------------------------------------------------------
-- Authentication -------------------------------------------------------------
-------------------------------------------------------------------------------

function make_authenticator (sputnik)

   local passwords = sputnik:load_config_page(sputnik.config.PASS_PAGE_NAME)
   passwords = passwords.PASSWORDS or {}

   local function check_pw(username, password) 
      return md5.sumhexa(password) == passwords[username]
   end

   local function get_token(username) 
      return md5.sumhexa(username .. sputnik_config.SECRET_CODE) 
   end

   local function check_token(username, token)
      return get_token(username) == token
   end

   local function user_exists(username) 
      return passwords[username]
   end

   local function add_user(username, password) 
      passwords[username] = md5.sumhexa(password)
   end

   local function save(user)
      local password_page = {as_is={}}
      password_page.as_is.content = cosmo.fill(
	 [=[    PASSWORDS = {}
	        $do_passwords[[
                PASSWORDS["$user"]="$password"
         ]]]=],
	 { do_passwords = function() 
		for user, password in pairs(passwords) do
		   cosmo.yield{user=user, password=password}
		end
	     end
	 }
      )
      password_page.as_is.author = user
      password_page.as_is.summary = "Added new user: "..user
      password_page.as_is.metapage = "Meta:Code"
      password_page.as_is.title = "Special:Passwords"
      password_page.name = sputnik.config.PASS_PAGE_NAME 
      sputnik:save_page(password_page)
   end

   local function authenticate_by_password(user, password) 
      if check_pw(user, password) then
	 return user, get_token(user)
      elseif user_exists(user) then 
	 return nil
      else
	 add_user(user, password)
	 save(user)
	 return user, get_token(user)
      end
   end

   local function authenticate_by_token(user, token)
      if check_token(user, token) then
	 return user
      else
	 return nil
      end
   end

   return { 
      check_password = authenticate_by_password,
      check_token = authenticate_by_token 
   }
end


-------------------------------------------------------------------------------
--- THE SPUTNIK CLASS ---------------------------------------------------------
-------------------------------------------------------------------------------

Sputnik = {

   new = function(self, initial_config)
      local obj = {}
      setmetatable(obj, self)
      self.__index = self
      obj.config = initial_config
      local storage_module = require(initial_config.STORAGE_MODULE 
				     or "wikifilestorage")
      local storage = storage_module.open(initial_config.WIKI_DATA_DIR)
      obj.storage  = wikistorage.open(storage)
      obj.storage_module = storage_module
      require("sputnik_actions")
      obj.actions = sputnik_actions.actions
      obj.actions.__index = obj.actions  -- to use it as a metatable later
      obj.wrappers = sputnik_actions.wrappers
      obj:init()
      return obj
   end,

   init = function(self)

      local extra_config   = self:load_config_page(
			      self.config.CONFIG_PAGE_NAME,
			      {BASE_URL = self.config.BASE_URL}
			   )

      for k,v in pairs(extra_config) do
	 self.config[k] = v
      end

      self.markdown = function (text)
			 require("markdown")
			 return markdown(text)
		      end
   end,


   load_extra_actions = function(self, mod_name) 
      local m = require("sputnik_" .. mod_name)
      return m.actions
   end,

   --- Split ------------------------------------------------------------------
   ------------ Adapted from http://lua-users.org/wiki/SplitJoin


   split = function (self, text, delimiter)
      local list = {}
      local pos = 1
      if string.find("", delimiter, 1) then 
	 error("delimiter matches empty string!")
      end
      while 1 do
	 local first, last = string.find(text, delimiter, pos)
	 if first then -- found?
	    table.insert(list, string.sub(text, pos, first-1))
	    pos = last+1
	 else
	    table.insert(list, string.sub(text, pos))
	    break
	 end
      end
      return unpack(list)
   end,

   --- Escape a text for using in a textarea ----------------------------------

   escape = function(self, text) 
      return text:gsub("&", "&amp;"):gsub(">","&gt;"):gsub("<","&lt;")
   end,

   --- Escape a URL -----------------------------------------------------------

   escape_url = function (self, text) 
      return self:escape(text):gsub(" ","%%20")
   end,

   --- Turn a string of text into a wikilink ----------------------------------

   wikify = function (self, wikilink, templates)

      -- [[Page_Name.edit#A1|Edit the Page]]

      local title, page_name, exists, link

      wikilink, title   = self:split(wikilink, "|")
      wikilink, anchor  = self:split(wikilink, "#")
      page_name, action = self:split(wikilink, "%.")

      title = title or page_name
      page_name = self:dirify(page_name)

      exists = self.storage:page_exists(page_name)
      if exists then
	 link = self:make_link(page_name)
	 if anchor then
	    link = link.."#"..anchor
	 end
      else 
	 link = self:make_link(page_name)
      end
      return self:format_wikilink(title, link, exists, templates)
   end,

   --- Wikify a text string ---------------------------------------------------

   wikify_text = function (self, text, templates) 
      local content = string.gsub(text, "%[%[([^%]]*)%]%]", 
				  function(text) 
				     return self:wikify(text, templates)
				  end)
      require("markdown")
      return markdown(content)
   end,

   --- Make a wikilink from a table -------------------------------------------

   format_wikilink = function(self, title, link, exists, templates)
      local tab = { title = title, link = link}
      return cosmo.fill(templates.WIKI_LINK,
			{  if_page_exists = cosmo.cond(exists, tab),
			   if_no_such_page = cosmo.cond(not exists, tab) }
		     )
   end,

   --- Turn a string into something that can be used as a page name -----------

   dirify = function(self, text)
      -- convert ? + = % ' " / \ and spaces to '_'
      -- note this isn't enought to ensure legal win2k names, since we
      -- _are_ allowing [ ] + < > : ; *

      local pattern = [[[%?%+%=%%%s%'%"%/%\]+]]  -- 
      
      if text then
         return text:gsub(pattern, "_")
      else 
	 return ""
      end
   end,

   --- Make a url given a table of parameters ------------------------------- 

   make_url = function(self, page_name, action, params)
      page_name = self:dirify(page_name)
      local page_dot_action
      if action then 
         page_dot_action = page_name .. "." .. action
      else
         page_dot_action = page_name
      end
      if params and next(params) then
         local link = self.config.BASE_URL .. "?p=" .. page_dot_action
         for k, v in pairs(params or {}) do
            link = link .. "&" .. k .. "=" .. v
         end
         return link
      else
         return self.config.NICE_URL .. page_dot_action
      end
   end,

   --- Make a link given a table of parameters ------------------------------- 

   make_link = function(self, page_name, action, params)
      local url = self:make_url(page_name, action, params)
      return string.format("href='%s' class='local'", url)
   end,

   --- Check is the page is a "Talk:" page ------------------------------------

   is_talk = function(self, name)
      return name and name:len() > 5 and name:sub(1,5) == "Talk:"
   end,

   --- Get the name of the corresponding "Talk:" page -------------------------

   talk = function(self, name) 
      return "Talk:" .. self:untalk(name)
   end,

   --- Get the "normal" page for this "Talk:" page ----------------------------

   untalk = function(self, name) 
      if self:is_talk(name) then 
	 return name:sub(6) 
      else
	 return name
      end
   end,

   --- Add to a page all it needs to be used by actions -----------------------

   decorate_page = function(self, page, params)

      params = params or {}

      -- Make a new page that will use old page as a prototype.
      -- This way we can make changes to the new page but still 
      -- have the old page intact if we need it.

      local new_page = {as_is = page}
      setmetatable(new_page, page)
      page.__index = page
      
      -- the properties of new_page need to be augmented by values from the 
      -- metapage chain.
      -- (strictly speaking, the metapage values will come first, then the 
      --  ones set in this page.. this allows overriding in a sensible way
      --  without eliminating the basic complementing functionality)
      new_page = self:augment_from_metapage(new_page)
      
      local templates = self:load_config_page(self.config.DEFAULT_TEMPLATE_SET)
                            
      if page.templates then
	 for k, v in pairs(self:load_config_page(page.templates)) do
	    templates[k] = v
	 end
      end
      new_page.templates = templates   -- not the same as page.templates!

      local language = params.language or self.config.INTERFACE_LANGUAGE
      for k, template in pairs(new_page.templates) do
	 new_page.templates[k] = self:translate(template, 
						"Translations:Main",
						language)
      end

      if page.actions and type(page.actions) == "string" then
	 new_page.actions = self:load_config(page.actions)
      else
	 new_page.actions = {}
      end
      
      -- self.actions is the actions we loaded sputnik_actions
      -- it's not the same as new_page.actions, which is just a table
      -- of function names

      local actions = {}
      setmetatable(actions, self.actions)

      for k, v in pairs(new_page.actions) do
	 local mod_name, dot_action = self:split(v, "%.")
	 if dot_action then
	    actions[k] = self:load_extra_actions(mod_name)[dot_action]
	 else
	    actions[k] = actions[v]
	 end
      end

      new_page.actions = actions
      new_page.wrappers = self.wrappers
      return new_page
   end,

   --- Chain together defaulted values from the metapages of this page --------
   
   augment_from_metapage = function(self, page)
      if page.metapage and self.storage:page_exists(page.metapage) then
         history = self.storage:get_page_history(page.metapage)
         metapage = history:load_latest()
         -- augment from all chained metapages of this metapage
         -- (which is a no-op if this is the lowest metapage level)
         metapage = self:augment_from_metapage(metapage)
         
         -- do the current level of augmentation
         -- meta values go first, then .. in the current page vals
         -- note we don't just dump *all* values in.. we'd then 
         -- have to filter out things like .name and .version
         -- that are inserted by the storage object
         for i, field in ipairs(self.storage:get_saveable_fields()) do
            if metapage[field] then 
               page[field] = metapage[field] .. (page[field] or "")
            end
         end
      end
      return page
   end,

   --- Get the page with this name (undecorated) ------------------------------

   get_page = function(self, page_name, version) 
      local page = self.storage:load_page_or_new(page_name, version)
      self:add_urls(page)
      self:add_links(page)
      return page

   end,

   --- This function will add a "urls" fields to a page, making ---------------
   --
   --      page.urls:foo(params)
   --
   --  equivalent to
   --
   --      sputnik:make_url(page.name, "foo", params)
   --
   --  for ANY foo.
   ----------------------------------------------------------------------------

   add_urls = function(self, page)
      page.urls = {}
      setmetatable(page.urls,
		   { __index = function(table, key)
			return function(inner_self, params)
				  return self:make_url(page.name, 
						       key, 
						       params)
			       end
		     end
		   }
		)
      return page
   end,

   --- Now same for "make_link" ----------------------------------------------

   add_links = function(self, page)
      page.links = {}
      setmetatable(page.links,
		   { __index = function(table, key)
			return function(inner_self, params)
				  return self:make_link(page.name, 
						        key, 
						        params)
			       end
		     end
		   }
		)
      return page
   end,

   --- Generates page-like table to make urls --------------------------------

   pseudo_page = function(self, page_name)
      local page = {name = page_name }
      self:add_urls(page)
      self:add_links(page)
      return page
   end,

   --- Update page with values from params table ------------------------------

   update_page_with_params = function(self, page, params)
      -- do not hardcode field names here; leads to bugs
      local new_page = { as_is = {} }
      params.author = params.user
      for i, field in ipairs(self.storage:get_saveable_fields()) do
	 new_page.as_is[field] = params[field] or page.as_is[field]
	 --raw = raw .. "\n" .. field .. "=" .. new_page.as_is[field]
      end
      local raw = self.storage:page_to_string(new_page)
      new_page = self.storage:string_to_page(page.name, raw)
      --new_page = self:decorate_page(new_page, params)
      --raw = raw .. "\n--------\n" .. self.storage:page_to_string(new_page)

      self:add_urls(new_page)
      self:add_links(new_page)
      return new_page
   end,

   --- Save the page (takes undecorated page as a parameter) ------------------

   save_page = function(self, page, raw_page)
      if raw_page then
	 return self.storage:save_page(page.name, raw_page)
      else
	 return self.storage:save_page(page.name, page)
      end
   end,

   --- Translate a template into a given language -----------------------------

   translate = function(self, template, translation_page, lang) 

      translations = self:load_config_page(translation_page)

      -- assemble a list of languages that we'll fall back on,
      -- e.g., {"pt_PT", "pt_BR", "en_US"} (i.e., try pt_PT, 
      -- then pt_BR, then en_US.)

      local fallback_chain = {}
      translations.FALLBACKS = translations.FALLBACKS or {}
      translations.FALLBACKS._all = translations.FALLBACKS._all or "en_US"
      if lang then 
	 fallback_chain[1] = lang 
	 local prefix = lang
	 while prefix do
	    if prefix and translations.FALLBACKS[prefix] then
	       table.insert(fallback_chain, translations.FALLBACKS[prefix])
	    end
	    prefix, _ = prefix:match("(.+)_([^_]*)")
	 end
      end
      table.insert(fallback_chain, translations.FALLBACKS._all)

      return string.gsub(template, "_%(([%w_%d]+)([^%)]*)%)",
			 function(key)
			    if not translations[key] then 
			       return "_("..key..")"
			    else
			       for i, lang in ipairs(fallback_chain) do
				  if translations[key][lang] then
				     return translations[key][lang]
				  end
			       end
			       return "%("..key..")"
			    end
			 end
		      )
   end,

   --- Returns page history ---------------------------------------------------

   get_history = function(self, page_name, limit, date)
      local pages = self.storage:get_history(page_name, limit, date)
      for i, p in ipairs(pages) do
	 self:add_urls(p)
         self:add_links(p)
      end
      return pages
   end,

   --- Parse safely Lua code in a string and use it as table -----------------

   load_config = function(self, text, env)
      return self.storage:load_config(text, env or {})
   end,

   --- Load page and parse it's content as if it was a config -----------------

   load_config_page = function(self, page_name, env)
      return self:load_config(self:get_page(page_name).content, env)
   end,
}

--- END OF THE SPUTNIK CLASS (the rest is just details) -----------------------


-------------------------------------------------------------------------------
-- A function to handle the CGI parameters (and do the authentication) --------
-------------------------------------------------------------------------------

function get_params (cgi, sputnik)

   local page_name, action
   if cgi.p then -- "nice" url
      page_name, action = sputnik:split(cgi.p, "%.")
   else
      page_name, action = cgi.page, cgi.action
   end

   if not page_name or page_name:len() == 0 then 
      page_name = sputnik.config.HOME_PAGE 
   end
   if not action then action = "show" end

   local params = {}
   for k, v in pairs(cgi) do
      params[k] = v
   end

   params.page = nil
   params.user = nil
   params.alleged_user = cgi.user
   params.page_name = page_name
   if action:len() > 0 then
      params.action = action
   else
      params.action = nil
   end

   params.user_cookie, params.auth_token = sputnik:split(
		cgilua.cookies.get(sputnik.config.COOKIE_NAME) or "", 
		"|")

			
					       

   local auth = make_authenticator(sputnik)
   if cgi.user and cgi.user:len() > 0 then
      params.user, params.auth_token = auth.check_password(cgi.user, cgi.pw)
   elseif params.auth_token then
      params.user = auth.check_token(params.user_cookie, params.auth_token)
   else 
      params.user = nil
   end

   return params
end


--------------------------------------------------- "MAIN" ----------

require("lfs")        -- luaFileSystem
require("cgilua.cookies")
require("md5")
require("cosmo")
require("wikistorage")
require("sputnik_actions")
require("sputnik_config")
require("markdown")



function main (cgi)

   local sputnik = Sputnik:new(sputnik_config)
   local params = get_params(cgi, sputnik)
   local page = sputnik:get_page(params.page_name, params.version)
   page = sputnik:decorate_page(page, params)

   local action_function = page.actions[params.action or "show"]
                           or sputnik_actions.actions.action_not_found

   local content, content_type = action_function(page, params, sputnik)
                                             
   if not content_type or type(content_type) == "number" then 
      content_type = "text/html" 
   end			   

   local type, subtype = sputnik:split(content_type, "/")
   cgilua.contentheader(type, subtype)
   cgilua.put(content)
end

------------------------------------------------------------------------

main(cgi)

